// Written in the D Programming Language
/**
 * The 10h lesson in the <a href="http://nehe.gamedev.net/data/lessons/lesson.asp?lesson=10">NeHe tutorial series</a>.
 * Originally written by Lionel Brits and Jeff Molofee.
 *
 * Authors: Lionel Brits
 *          Jeff Molofee
 *          Olli Aalto
 */
module lesson10;

import derelict.opengl.gl;
import derelict.opengl.glu;
import derelict.sdl.sdl;

import tango.stdc.stringz;
import tango.stdc.stdio;
import tango.text.stream.LineIterator;
import tango.io.Buffer;
import Integer = tango.text.convert.Integer;
import Float = tango.text.convert.Float;
import Math = tango.math.Math;
import TextUtil = tango.text.Util;

/// Build Our Vertex Structure
struct Vertex
{
	/// 3D Coordinates
	float x = 0.0f, y = 0.0f, z = 0.0f;
	/// Texture Coordinates
	float u = 0.0f, v = 0.0f;
}

/// Build Our Triangle Structure 
struct Triangle
{
	/// Array Of Three Vertices	
	Vertex[3] vertices;
}

/// Build Our Sector Structure 
struct Sector
{
	/// An Array Of Triangles
	Triangle[] triangles;
}

/// Our sector 
Sector sector;

/// Camera rotation variable
GLfloat yrot = 0.0f;
/// Camera pos variable
GLfloat xpos = 0.0f, zpos = 0.0f;

/// Head-bobbing variables
GLfloat walkBias = 0.0f, walkBiasAngle = 0.0f;
GLfloat lookUpDown = 0.0f;

/// Ambient Light Values
GLfloat lightAmbient[] = [0.5f, 0.5f, 0.5f, 1.0f];
/// Diffuse Light Values
GLfloat lightDiffuse[] = [1.0f, 1.0f, 1.0f, 1.0f];
/// Light Position
GLfloat lightPosition[] = [0.0f, 0.0f, 2.0f, 1.0f];

/// Constant used for converting to radians
const float piover180 = 0.0174532925f;

bool blend;
bool light;

/// Which Filter To Use
GLuint filter;
/// Storage for 3 textures
GLuint[3] textures;

const char[]
	WINDOW_TITLE = "Lionel Brits & NeHe's 3D World Tutorial (D version)";
bool running = true;

/**
 * Module constructor. Here we load the GL, GLU and SDL shared libraries,
 * and the initialize SDL.
 */
static this()
{
	DerelictGL.load();
	DerelictGLU.load();
	DerelictSDL.load();

	if (SDL_Init(SDL_INIT_VIDEO) < 0)
	{
		throw new Exception("Failed to initialize SDL: " ~ getSDLError());
	}
	// Enable key repeat so the player can keep a key down for moving
	if (SDL_EnableKeyRepeat(SDL_DEFAULT_REPEAT_DELAY,
		SDL_DEFAULT_REPEAT_INTERVAL))
	{
		throw new Exception("Failed to set key repeat: " ~ getSDLError());
	}
}

/**
 * Module destructor. SDL_Quit must be called somewhere, and as we initialized
 * it in the module constructor so the module destructor should be a suitable
 * place.
 */
static ~this()
{
	SDL_Quit();
}

/**
 * The main function. This is where the fun begins. The first order of business
 * is the check the command line arguments if the user wanted to start in
 * fullscreen mode. Then the window is created and OpenGL is initialized with
 * basic settings. Finally the the function starts the main loop which will live
 * for the duration of the application.
 *
 * Params:
 *      args = the command line arguments
 */
void main(char[][] args)
{
	bool fullScreen = false;

	if (args.length > 1)
	{
		fullScreen = args[1] == "-fullscreen";
	}

	createGLWindow(WINDOW_TITLE, 640, 480, 16, fullScreen);
	initGL();

	// We import the world data file at compile time.
	// Depending on the version of DMD you have installed,
	// you might need to use the -J switch or add the relative path
	// before the file name.
	// Version 1.005 of DMD is required for this.
	setupWorld(import("world.txt"));

	while (running)
	{
		processEvents();

		drawGLScene();

		SDL_GL_SwapBuffers();
		SDL_Delay(10);
	}
}

void processEvents()
{
	SDL_Event event;
	while (SDL_PollEvent(&event))
	{
		switch (event.type)
		{
			case SDL_KEYDOWN:
				keyPressed(event.key.keysym.sym);
				break;
			case SDL_QUIT:
				running = false;
				break;
			default:
				break;
		}
	}
}

void keyPressed(int key)
{
	switch (key)
	{
		case SDLK_ESCAPE:
			running = false;
			break;
		case SDLK_b:
			blend = !blend;
			if (blend)
			{
				glEnable(GL_BLEND);
				glDisable(GL_DEPTH_TEST);
			}
			else
			{
				glDisable(GL_BLEND);
				glEnable(GL_DEPTH_TEST);
			}
			break;
		case SDLK_f:
			filter = ++filter % 3;
			break;
		case SDLK_l:
			light = !light;
			if (light)
			{
				glEnable(GL_LIGHTING);
			}
			else
			{
				glDisable(GL_LIGHTING);
			}
			break;
		case SDLK_RIGHT:
			yrot -= 1.5f;
			break;
		case SDLK_LEFT:
			yrot += 1.5f;
			break;
		case SDLK_UP:
			xpos -= cast(float) Math.sin(yrot * piover180) * 0.05f;
			zpos -= cast(float) Math.cos(yrot * piover180) * 0.05f;
			if (walkBiasAngle >= 359.0f)
			{
				walkBiasAngle = 0.0f;
			}
			else
			{
				walkBiasAngle += 10;
			}

			walkBias = cast(float) Math.sin(walkBiasAngle * piover180) * 0.05f;
			break;
		case SDLK_DOWN:
			xpos += cast(float) Math.sin(yrot * piover180) * 0.05f;
			zpos += cast(float) Math.cos(yrot * piover180) * 0.05f;
			if (walkBiasAngle <= 1.0f)
			{
				walkBiasAngle = 359.0f;
			}
			else
			{
				walkBiasAngle -= 10;
			}

			walkBias = cast(float) Math.sin(walkBiasAngle * piover180) * 0.05f;
			break;
		default:
			break;
	}
}

void loadGLTextures()
{
	SDL_Surface* textureImage;

	if ((textureImage = SDL_LoadBMP("data/Mud.bmp")) !is null)
	{
		// Free the surface when exiting the scope
		scope(exit)
			SDL_FreeSurface(textureImage);

		// Create The Texture
		glGenTextures(3, &textures[0]);

		// Load in texture 1
		glBindTexture(GL_TEXTURE_2D, textures[0]);

		// Generate The Texture
		glTexImage2D(GL_TEXTURE_2D, 0, 3, textureImage.w, textureImage.h, 0,
			GL_BGR, GL_UNSIGNED_BYTE, textureImage.pixels);

		// Nearest Filtering
		glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
		glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);

		// Load in texture 2
		glBindTexture(GL_TEXTURE_2D, textures[1]);

		// Linear Filtering
		glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
		glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);

		// Generate The Texture
		glTexImage2D(GL_TEXTURE_2D, 0, 3, textureImage.w, textureImage.h, 0,
			GL_BGR, GL_UNSIGNED_BYTE, textureImage.pixels);

		// Load in texture 3
		glBindTexture(GL_TEXTURE_2D, textures[2]);

		// Mipmapped Filtering
		glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER,
			GL_LINEAR_MIPMAP_NEAREST);
		glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);

		// Generate The MipMapped Texture
		gluBuild2DMipmaps(GL_TEXTURE_2D, 3, textureImage.w, textureImage.h,
			GL_BGR, GL_UNSIGNED_BYTE, textureImage.pixels);

		return;
	}
	throw new Exception("Failed to load textures: " ~ getSDLError());
}

/**
 * Resize And Initialize The GL Window.
 */
void resizeGLScene(GLsizei width, GLsizei height)
{
	if (height == 0)
	{
		height = 1;
	}
	// Reset The Current Viewport
	glViewport(0, 0, width, height);

	// Select The Projection Matrix
	glMatrixMode(GL_PROJECTION);
	// Reset The Projection Matrix
	glLoadIdentity();

	// Calculate The Aspect Ratio Of The Window
	gluPerspective(45.0f, cast(GLfloat) width / cast(GLfloat) height, 0.1f,
		100.0f);

	// Select The Modelview Matrix
	glMatrixMode(GL_MODELVIEW);
	// Reset The Modelview Matrix
	glLoadIdentity();
}

/**
 * All Setup For OpenGL Goes Here.
 */
void initGL()
{
	loadGLTextures();

	/* Enable Texture Mapping */
	glEnable(GL_TEXTURE_2D);

	/* Enable smooth shading */
	glShadeModel(GL_SMOOTH);

	/* Set the background black */
	glClearColor(0.0f, 0.0f, 0.0f, 0.0f);

	/* Depth buffer setup */
	glClearDepth(1.0f);

	/* Enables Depth Testing */
	glEnable(GL_DEPTH_TEST);

	/* The Type Of Depth Test To Do */
	glDepthFunc(GL_LEQUAL);

	/* Really Nice Perspective Calculations */
	glHint(GL_PERSPECTIVE_CORRECTION_HINT, GL_NICEST);

	/* Setup The Ambient Light */
	glLightfv(GL_LIGHT1, GL_AMBIENT, lightAmbient.ptr);

	/* Setup The Diffuse Light */
	glLightfv(GL_LIGHT1, GL_DIFFUSE, lightDiffuse.ptr);

	/* Position The Light */
	glLightfv(GL_LIGHT1, GL_POSITION, lightPosition.ptr);

	/* Enable Light One */
	glEnable(GL_LIGHT1);

	/* Full Brightness, 50% Alpha */
	glColor4f(1.0f, 1.0f, 1.0f, 0.5f);

	/* Blending Function For Translucency Based On Source Alpha Value */
	glBlendFunc(GL_SRC_ALPHA, GL_ONE);

	// Change to texture matrix and flip and rotate the texture
	glMatrixMode(GL_TEXTURE);
	glRotatef(180.0f, 0.0f, 0.0f, 1.0f);
	glScalef(-1.0f, 1.0f, 1.0f);
	// Back to normal
	glMatrixMode(GL_MODELVIEW);
}

/**
 * Here's Where We Do All The Drawing.
 */
void drawGLScene()
{
	/* Used For Player Translation On The X Axis */
	GLfloat xtrans = -xpos;
	/* Used For Player Translation On The Z Axis */
	GLfloat ztrans = -zpos;
	/* Used For Bouncing Motion Up And Down */
	GLfloat ytrans = -walkBias - 0.5f;
	/* 360 Degree Angle For Player Direction */
	GLfloat sceneRot = 360.0f - yrot;

	/* Clear The Screen And The Depth Buffer */
	glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
	glLoadIdentity();

	/* Rotate Up And Down To Look Up And Down */
	glRotatef(lookUpDown, 1.0f, 0.0f, 0.0f);
	/* Rotate Depending On Direction Player Is Facing */
	glRotatef(sceneRot, 0.0f, 1.0f, 0.0f);

	/* Translate The Scene Based On Player Position */
	glTranslatef(xtrans, ytrans, ztrans);

	/* Select A Texture Based On filter */
	glBindTexture(GL_TEXTURE_2D, textures[filter]);

	/* Process Each Triangle */
	foreach (t; sector.triangles)
	{
		/* Normal Pointing Forward */
		glNormal3f(0.0f, 0.0f, 1.0f);
		/* Start Drawing Triangles */
		glBegin(GL_TRIANGLES);
		foreach (ver; t.vertices)
		{
			/* Set The TexCoords And Vertices */
			glTexCoord2f(ver.u, ver.v);
			glVertex3f(ver.x, ver.y, ver.z);
		}
		glEnd();
	}
}

void createGLWindow(char[] title, int width, int height, int bits,
	bool fullScreen)
{
	// Set the OpenGL attributes
	SDL_GL_SetAttribute(SDL_GL_RED_SIZE, 8);
	SDL_GL_SetAttribute(SDL_GL_GREEN_SIZE, 8);
	SDL_GL_SetAttribute(SDL_GL_BLUE_SIZE, 8);
	SDL_GL_SetAttribute(SDL_GL_DEPTH_SIZE, 16);
	SDL_GL_SetAttribute(SDL_GL_DOUBLEBUFFER, 1);

	// Set the window title
	SDL_WM_SetCaption(toStringz(title), null);

	/* Note the SDL_DOUBLEBUF flag is not required to enable double
	 * buffering when setting an OpenGL video mode.
	 * Double buffering is enabled or disabled using the
	 * SDL_GL_DOUBLEBUFFER attribute.
	 */
	int mode = SDL_OPENGL;
	if (fullScreen)
	{
		mode |= SDL_FULLSCREEN;
	}
	// Now open a SDL OpenGL window with the given parameters
	if (SDL_SetVideoMode(width, height, bits, mode) is null)
	{
		throw new Exception("Failed to open OpenGL window: " ~ getSDLError());
	}

	resizeGLScene(width, height);
}

/**
 * Read lines from the iterator until one is found that isn't empty or does not 
 * start with '/' character.
 */
char[] readLine(LineIterator!(char) iter)
{
	char[] line;
	do
	{
		line = iter.next();
	}
	while (line.length == 0 || line[0] == '/');
	return TextUtil.trim(line);
}

/**
 * Parse the input data and initialize the Sector with Triangles and Vertices.
 */
void setupWorld(char[] worldData)
{
	// Create a iterator that iterates over every line in the input data.
	auto iter = new LineIterator!(char)(new Buffer(worldData));
	// Split the first line
	auto splittedLine = TextUtil.delimit!(char)(readLine(iter), " ");
	// Parse the number of triangles from the first line
	sector.triangles.length = Integer.parse(splittedLine[1]);

	foreach (inout tri; sector.triangles)
	{
		foreach (inout ver; tri.vertices)
		{
			// Read the vertex and texture position data to ver's fields.
			sscanf(readLine(iter).ptr, "%f%f%f%f%f", &ver.x, &ver.y, &ver.z,
				&ver.u, &ver.v);
		}
	}
}

/**
 * Get the SDL error as a D string.
 *
 * Returns: A D string containing the current SDL error.
 */
char[] getSDLError()
{
	return fromStringz(SDL_GetError());
}
